此文來自這位 youtuber BK Codes 的內容;但其實也不算翻譯,因為他的印度口音我還真的完全對不上頻率!! 因此主要為以我為小白角度將 code 上註解,若有誤解之處歡迎各位讀著更正賜教!
第二篇為 FLUTTER 部分,記得開啟 Django 後台(第一篇 Django 部分)
lib
|_ api
|_api.dart # 連接後台,傳遞資料增減
|_ models
|_todo.dart # 序列化輸入資料 model
|_ screens
|_addTodo.dart # 開啟 screen 輸入增加資料
|_ main.dart # 主程式
dependencies:
flutter:
sdk: flutter
http: ^0.12.2 # 網絡請求
provider: ^4.3.1 # 組件狀態共享
cupertino_icons: ^0.1.3 # icon
import 'dart:convert';
import 'package:flutter/material.dart';
import '../models/todo.dart';
import 'package:http/http.dart' as http;
class TodoProvider with ChangeNotifier {
TodoProvider() {
this.fetchTasks();
}
List<Todo> _todos = [];
List<Todo> get todos { //使用get作為字首,運行屬性訪問器函式 getter
// 向外暴露類中某個狀態適合使用setter,getter函式
// 如果是觸發類中的某個行為操作普通函式較適合。
return [..._todos]; //展開運算子 (Spread Operator),將兩個 List 結合在一起: [...list]
} // main.dart 訪問並回傳資料
void addTodo(Todo todo) async {
final response = await http.post('http://10.0.2.2:8000/apis/v1/',
headers: {"Content-Type": "application/json"}, body: json.encode(todo));
if (response.statusCode == 201) {
//201 Created 成功狀態碼表示請求成功且有一個新的資源已經依據需要而被建立。
todo.id = json.decode(response.body)['id']; //當資料產生時 django 會賦予一個 id 和 pk
_todos.add(todo); //加入新輸入的 model
notifyListeners(); //重新渲染
}
}
void deleteTodo(Todo todo) async {
final response =
await http.delete('http://10.0.2.2:8000/apis/v1/${todo.id}/');
if (response.statusCode == 204) {
//204 No Content 成功狀態碼表明請求成功,但客戶端不需要更新目前的頁面。
_todos.remove(todo);
notifyListeners();
}
}
fetchTasks() async {
final url = 'http://10.0.2.2:8000/apis/v1/?format=json';
final response = await http.get(url);
if (response.statusCode == 200) {
var data = json.decode(response.body) as List;
//print('data::$data');
// data 輸出為 [json, json, json...]
_todos = data.map<Todo>((json) => Todo.fromJson(json)).toList();
//print('_todos::$_todos');
// _todos 輸出為 [Instance, Instance, Instance...]
}
}
}
// convert Map to List of Objects
// https://stackoverflow.com/questions/57234575/dart-convert-map-to-list-of-objects
class Todo {
int id;
final String title;
final String description;
Todo({this.id, this.title, this.description});
factory Todo.fromJson(Map<String, dynamic> json) {
return Todo(
id: json['id'], title: json['title'], description: json['description']);
}
dynamic toJson() => {'id': id, 'title': title, 'description': description};
}
// factory 工廠構造函數
// 工廠構造函數不能和使用this關鍵字來使用class的屬性和方法。只能使用類中static類型的屬性和方法。抽像類只能被繼承或者當做接口,不能被實例化。但是抽像類中的工廠構造函數是可以實例化(return)如果抽像類使用factory 類名._(){ return null;},這個抽像類將不能被繼承,只能當做接口
// https://juejin.cn/post/6844904017991221255
// 抽像類 Todo, 用以驗證輸入格式是否正確(int, String...)
// factory 用於將輸入內容物返回實例化
import 'package:app/api/api.dart';
import 'package:app/models/todo.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AddTodoScreen extends StatefulWidget {
@override
_AddTodoScreenState createState() => _AddTodoScreenState();
}
class _AddTodoScreenState extends State<AddTodoScreen> {
final todoTitleController = TextEditingController();
final todoDesController = TextEditingController();
// TextEditingController : 響應文本框內容的更改,據輸入內容來更新結果
void onAdd() {
final String textVal = todoTitleController.text;
final String desVal = todoDesController.text;
if (textVal.isNotEmpty && desVal.isNotEmpty) {
final Todo todo = Todo(title: textVal, description: desVal); // 放入 models Todo
Provider.of<TodoProvider>(context, listen: false).addTodo(todo);
// Provider.of with listen:false : 調用但不使 widget 被重構
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Todo')),
body: ListView(
children: [
Container(
child: Column(
children: [
TextField(
controller: todoTitleController,
// TextEditingController 綁定 TextField,開始監聽文本框的變化。
),
TextField(
controller: todoDesController,
),
RaisedButton(
child: Text('Add'),
onPressed: () {
onAdd();
Navigator.of(context).pop();
// 執行加入資料後
// Navigator.pop, 刪除目前 screen, 回到 list
})
],
))
],
),
);
}
}
6.main.dart
import 'package:app/api/api.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './screens/addTodo.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider( //ChangeNotifierProvider 可向其所有子節點指定聽取一個 ChangeNotifier
create: (context) => TodoProvider(), // api.dart 所建構的 ChangeNotifier 指定於此
child: MaterialApp( // MaterialApp 控制主題色彩等等
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final todoP = Provider.of<TodoProvider>(context); // 監控 TodoProvider
return Scaffold(
appBar: AppBar(
title: Text('Todo App'),
),
body: ListView.builder(
shrinkWrap: true,
itemCount: todoP.todos.length,
itemBuilder: (BuildContext context, int index) {
//print('index::$index'); // itemBuilder 捲到才顯示該資料
return ListTile(
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
todoP.deleteTodo(todoP.todos[index]);
// todoP.todos 為一個 list [Instance, Instance, Instance...]
// 固可由 ListView 的 index 指定該資料操作
}),
title: Text(
todoP.todos[index].title,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
subtitle: Text(
todoP.todos[index].description,
style: TextStyle(fontSize: 15, color: Colors.black),
));
},
),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.add,
size: 30,
),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (ctx) => AddTodoScreen()));
// 當使用 Navigator 導向其他 screen 時有二種方式可使用
// Navigator.push, 直接疊上新 screen
// Navigator.pop, 刪除目前 screen/
// MaterialPageRoute 指用 context 的規則不甚瞭解,也未找到想詳細資料,有待釐清
}),
);
}
}
註: 資料出處
Part1: https://www.youtube.com/watch?v=hfee7SIwUTs
Part2: https://www.youtube.com/watch?v=fyndW3s9t6M
Part3: https://www.youtube.com/watch?v=LW-220zNA2E
GITHUB:https://github.com/bayardkalyan/flutter-django-fullstack